{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 7,
   "outputs": [],
   "source": [
    "import sys\n",
    "import torch\n",
    "from torch import nn\n",
    "import os\n",
    "import math\n",
    "\n",
    "sys.path.append(os.path.abspath(\"../../data\"))\n",
    "\n",
    "DEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "BATCH_SIZE = 64"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading...\n",
      "From: https://drive.google.com/uc?export=download&id=1O_uOTvMJb2FkUK7rB6lMqpPQqiAdLXNL\n",
      "To: /Users/vik/Personal/nnets/data/clean_weather.csv\n",
      "100%|██████████| 406k/406k [00:00<00:00, 5.13MB/s]\n"
     ]
    }
   ],
   "source": [
    "from csv_data import WeatherDatasetWrapper\n",
    "\n",
    "class WeatherDataset(WeatherDatasetWrapper):\n",
    "    predictors = [\"tmax\", \"tmin\", \"rain\"]\n",
    "    target = \"tmax_tomorrow\"\n",
    "    sequence_length = 7\n",
    "\n",
    "wrapper = WeatherDataset(DEVICE)\n",
    "datasets = wrapper.generate_datasets(BATCH_SIZE)\n",
    "train = datasets[\"train\"]\n",
    "valid = datasets[\"validation\"]"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "outputs": [],
   "source": [
    "class GRUCell(nn.Module):\n",
    "    def __init__(self, input_units, hidden_units, output_units):\n",
    "        super(GRUCell, self).__init__()\n",
    "        self.input_units = input_units\n",
    "        self.hidden_units = hidden_units\n",
    "        self.output_units = output_units\n",
    "\n",
    "        k = math.sqrt(1/hidden_units)\n",
    "        self.input_weights = nn.Parameter(torch.rand(3, input_units, hidden_units) * 2 * k - k)\n",
    "        self.input_biases = nn.Parameter(torch.rand(3, 1, hidden_units) * 2 * k - k)\n",
    "\n",
    "        self.hidden_weights = nn.Parameter(torch.rand(3, hidden_units, hidden_units) * 2 * k - k)\n",
    "        self.hidden_biases = nn.Parameter(torch.rand(3, 1, hidden_units) * 2 * k - k)\n",
    "\n",
    "    def forward(self, x, prev_hidden):\n",
    "        # Compute the regular RNN forward pass\n",
    "        # Compute update and reset gates for GRU\n",
    "        reset_gate = torch.sigmoid(x @ self.input_weights[0,] + self.input_biases[0,] + prev_hidden @ self.hidden_weights[0,] + self.hidden_biases[0,])\n",
    "        update_gate = torch.sigmoid(x @ self.input_weights[1,] + self.input_biases[1,] + prev_hidden @ self.hidden_weights[1,] + self.hidden_biases[1,])\n",
    "        new_gate = torch.tanh(x @ self.input_weights[2,] + self.input_biases[2,] + torch.mul(reset_gate, prev_hidden @ self.hidden_weights[2,] + self.hidden_biases[2,]))\n",
    "\n",
    "        hidden_x = torch.mul((1 - update_gate), new_gate) + torch.mul(update_gate, new_gate)\n",
    "        return hidden_x"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "outputs": [],
   "source": [
    "class Network(nn.Module):\n",
    "    def __init__(self, sequence_len, input_units, output_units, hidden_units=512, layers=2):\n",
    "        super(Network, self).__init__()\n",
    "        self.sequence_len = sequence_len\n",
    "        self.hidden_units = hidden_units\n",
    "        self.input_units = input_units\n",
    "        self.output_units = output_units\n",
    "        self.layers = layers\n",
    "\n",
    "        self.linear_encode = nn.Linear(in_features=input_units, out_features=hidden_units)\n",
    "\n",
    "        self.gru = GRUCell(input_units=hidden_units, hidden_units=hidden_units, output_units=hidden_units)\n",
    "        self.linear_decode = nn.Linear(in_features=hidden_units, out_features=output_units)\n",
    "\n",
    "    def forward(self, x):\n",
    "        batch_size = x.shape[0]\n",
    "        # Embed the input sequence to reduce dimensionality\n",
    "        encoded = self.linear_encode(x).swapaxes(0,1)\n",
    "\n",
    "        # Encode the input sequence\n",
    "        # Both tensors will have sequence then batch\n",
    "        hiddens = torch.zeros((1, batch_size, self.hidden_units), device=DEVICE)\n",
    "        outputs = torch.zeros((1, batch_size, self.output_units), device=DEVICE)\n",
    "        for j in range(self.sequence_len):\n",
    "            hidden = self.gru(encoded[j,:], hiddens[j,])\n",
    "            # Add first sequence axis\n",
    "            output = self.linear_decode(hidden).unsqueeze(0)\n",
    "            hidden = hidden.unsqueeze(0)\n",
    "            outputs = torch.cat((outputs, output), dim=0)\n",
    "            hiddens = torch.cat((hiddens, hidden), dim=0)\n",
    "\n",
    "        # Move batch back to axis 0, and trim first element\n",
    "        out_hiddens = hiddens[1:,:,:].swapaxes(0,1)\n",
    "        out_output = outputs[1:,:,:].swapaxes(0,1)\n",
    "        return out_output, out_hiddens"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "outputs": [],
   "source": [
    "from tqdm.auto import tqdm\n",
    "model = Network(wrapper.sequence_length, input_units=len(wrapper.predictors), output_units=1, hidden_units=512, layers=1).to(DEVICE)\n",
    "loss_fn = nn.MSELoss()\n",
    "optimizer = torch.optim.AdamW(model.parameters(), lr=1e-2)"
   ],
   "metadata": {
    "collapsed": false
   }
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "148it [00:07, 18.64it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 0 train loss: 183.95619646278587 valid loss: 23.087440729141235\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "148it [00:07, 18.51it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1 train loss: 23.192032627157264 valid loss: 21.402359157800674\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "148it [00:08, 18.38it/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 2 train loss: 22.18263073869654 valid loss: 21.01702618598938\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "35it [00:01, 17.81it/s]\n"
     ]
    },
    {
     "ename": "KeyboardInterrupt",
     "evalue": "",
     "output_type": "error",
     "traceback": [
      "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m",
      "\u001B[0;31mKeyboardInterrupt\u001B[0m                         Traceback (most recent call last)",
      "Cell \u001B[0;32mIn[6], line 10\u001B[0m\n\u001B[1;32m      8\u001B[0m pred, hidden \u001B[38;5;241m=\u001B[39m model(sequence)\n\u001B[1;32m      9\u001B[0m loss \u001B[38;5;241m=\u001B[39m loss_fn(pred, target)\n\u001B[0;32m---> 10\u001B[0m \u001B[43mloss\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mbackward\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\n\u001B[1;32m     11\u001B[0m optimizer\u001B[38;5;241m.\u001B[39mstep()\n\u001B[1;32m     12\u001B[0m train_loss \u001B[38;5;241m+\u001B[39m\u001B[38;5;241m=\u001B[39m loss\u001B[38;5;241m.\u001B[39mitem()\n",
      "File \u001B[0;32m~/.virtualenvs/nnets/lib/python3.10/site-packages/torch/_tensor.py:488\u001B[0m, in \u001B[0;36mTensor.backward\u001B[0;34m(self, gradient, retain_graph, create_graph, inputs)\u001B[0m\n\u001B[1;32m    478\u001B[0m \u001B[38;5;28;01mif\u001B[39;00m has_torch_function_unary(\u001B[38;5;28mself\u001B[39m):\n\u001B[1;32m    479\u001B[0m     \u001B[38;5;28;01mreturn\u001B[39;00m handle_torch_function(\n\u001B[1;32m    480\u001B[0m         Tensor\u001B[38;5;241m.\u001B[39mbackward,\n\u001B[1;32m    481\u001B[0m         (\u001B[38;5;28mself\u001B[39m,),\n\u001B[0;32m   (...)\u001B[0m\n\u001B[1;32m    486\u001B[0m         inputs\u001B[38;5;241m=\u001B[39minputs,\n\u001B[1;32m    487\u001B[0m     )\n\u001B[0;32m--> 488\u001B[0m \u001B[43mtorch\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mautograd\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mbackward\u001B[49m\u001B[43m(\u001B[49m\n\u001B[1;32m    489\u001B[0m \u001B[43m    \u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mgradient\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mretain_graph\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mcreate_graph\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43minputs\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[43minputs\u001B[49m\n\u001B[1;32m    490\u001B[0m \u001B[43m\u001B[49m\u001B[43m)\u001B[49m\n",
      "File \u001B[0;32m~/.virtualenvs/nnets/lib/python3.10/site-packages/torch/autograd/__init__.py:197\u001B[0m, in \u001B[0;36mbackward\u001B[0;34m(tensors, grad_tensors, retain_graph, create_graph, grad_variables, inputs)\u001B[0m\n\u001B[1;32m    192\u001B[0m     retain_graph \u001B[38;5;241m=\u001B[39m create_graph\n\u001B[1;32m    194\u001B[0m \u001B[38;5;66;03m# The reason we repeat same the comment below is that\u001B[39;00m\n\u001B[1;32m    195\u001B[0m \u001B[38;5;66;03m# some Python versions print out the first line of a multi-line function\u001B[39;00m\n\u001B[1;32m    196\u001B[0m \u001B[38;5;66;03m# calls in the traceback and some print out the last line\u001B[39;00m\n\u001B[0;32m--> 197\u001B[0m \u001B[43mVariable\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43m_execution_engine\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mrun_backward\u001B[49m\u001B[43m(\u001B[49m\u001B[43m  \u001B[49m\u001B[38;5;66;43;03m# Calls into the C++ engine to run the backward pass\u001B[39;49;00m\n\u001B[1;32m    198\u001B[0m \u001B[43m    \u001B[49m\u001B[43mtensors\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mgrad_tensors_\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mretain_graph\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mcreate_graph\u001B[49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43minputs\u001B[49m\u001B[43m,\u001B[49m\n\u001B[1;32m    199\u001B[0m \u001B[43m    \u001B[49m\u001B[43mallow_unreachable\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[38;5;28;43;01mTrue\u001B[39;49;00m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43maccumulate_grad\u001B[49m\u001B[38;5;241;43m=\u001B[39;49m\u001B[38;5;28;43;01mTrue\u001B[39;49;00m\u001B[43m)\u001B[49m\n",
      "\u001B[0;31mKeyboardInterrupt\u001B[0m: "
     ]
    }
   ],
   "source": [
    "EPOCHS = 100\n",
    "\n",
    "for epoch in range(EPOCHS):\n",
    "    # Run over the training examples\n",
    "    train_loss = 0\n",
    "    for batch, (sequence, target) in tqdm(enumerate(train)):\n",
    "        optimizer.zero_grad()\n",
    "        pred, hidden = model(sequence)\n",
    "        loss = loss_fn(pred, target)\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "        train_loss += loss.item()\n",
    "\n",
    "    with torch.no_grad():\n",
    "        # Compute validation loss.  Unless you have a lot of training data, the validation loss won't decrease.\n",
    "        valid_loss = 0\n",
    "        for batch, (sequence, target) in enumerate(valid):\n",
    "            # Only feed in the first token of the actual target\n",
    "            pred, hidden = model(sequence)\n",
    "            loss = loss_fn(pred, target)\n",
    "            valid_loss += loss.item()\n",
    "        print(f\"Epoch {epoch} train loss: {train_loss / len(train)} valid loss: {valid_loss / len(valid)}\")"
   ],
   "metadata": {
    "collapsed": false
   }
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
